#!/usr/bin/python
#
# Management of session information.
#
#
import copy
import datetime

import gtk

import os
import re
import urlparse

if os.name == 'nt':
    from nt_session_management import *
elif os.name == 'posix':
    from posix_session_management import *

def get_children(item):
    """Returns the result of the get children function for the given item."""
    return item.get_children()

class State_Item(object):
    """Base class of items that may be stored in a state list."""
    # Empty definitions to be overridded by sub-classes as necessary
    name = ''
    path = None
    state = ''
    locked = False
    __children = []

    def get_children(self):
        """Return a list of elements in this group."""
        return self.__children

    children = property(get_children)

    def visit(self):
        """'Visits' the selected item."""
        # No default action
        pass

    def is_active(self):
        """Return 'active' state flag."""
        return False

class List_Node(State_Item):
    """Header item for use in list display."""
    is_monitored = False
    state_names = {False : ''}
    locked = ''

    def __init__(self, name, content):
        """Defines the name displayed and content list for this group."""
        self.name = name
        self.__children = [element for element in content]

    def update(self, content):
        """Update the contents of this node based on the new content."""
        self.__children = [element for element in content]        

    def get_children(self):
        """Return a list of elements in this group."""
        return self.__children

    children = property(get_children)
    
    def open(self):
        """Null function to handle selection of a list node row."""
        pass

class Document(object):
    """Manage documents accessed by a particular application."""
    true_strings = ['true', 'yes']
    is_monitored = True
    state_names = {False : 'Closed', True : 'Open'}
    children = []

    @classmethod
    def from_xml(cls, node, application):
        """Construct a Document object from an xml node"""
        title = None
        state = None
        locked = False
        for child in node.childNodes:
            if child.tagName == 'path':
                path = child.firstChild.nodeValue
            elif child.tagName == 'title':
                title = child.firstChild.nodeValue
            elif child.tagName == 'state':
                state = child.firstChild.nodeValue
            elif child.tagName == 'locked':
                locked = child.firstChild.nodeValue.lower() in Document.true_strings
                
        new_object = cls(path, application, title, state)
        if locked:
            new_object.locked = True

        return new_object

    def __init__(self, path, application, title=None, state=None):
        """Initialise the file object."""
        self.path = path
        self.application = application
        self.title = title
        if state:
            self.state = state
        else:
            self.state = 'Closed'
        self.locked = False

    def __eq__(self, other):
        """Return object equality based on file path."""
        return (type(self) == type(other)) and (self.path == other.path)

    def __hash__(self):
        """Return hash of file path."""
        return hash(self.path)

    def to_xml(self, document):
        """Returns an xml node reepresenting this file."""
        node = document.createElement('document')

        path = document.createElement('path')
        path.appendChild(document.createTextNode(str(self.path)))
        node.appendChild(path)

        if self.title:
            title = document.createElement('title')
            title.appendChild(document.createTextNode(str(self.title)))
            node.appendChild(title)
        
        state = document.createElement('state')
        state.appendChild(document.createTextNode(str(self.state)))
        node.appendChild(state)

        if self.locked:
            locked = document.createElement('locked')
            locked.appendChild(document.createTextNode(str(self.locked)))
            node.appendChild(locked)
        
        return node        

    def get_name(self):
        """Returns a name string for this file."""
        if self.title:
            name = self.title
        else:
            name = os.path.basename(self.path)
            name = os.path.splitext(name)[0]

        return name

    name = property(get_name)

    def restore(self):
        """Restore the document to the last saved state."""
        if self.state == 'Open':
            app = find_application_item(self.application.path)
            app.open(self.path)

    def is_active(self):
        """Return a flag indicating the state of this document."""
        result = False
        app = find_application(self.application.path)
        if app:
            for document in app.get_open_documents():
                if document.path == self.path:
                    result = True
                    break
        return result

    def open(self):
        """Opens the document in it's associated application."""
        app = get_application(self.application.path)
        app.open(self.path)

class Application(object):
    """Manage data associated with a particular application."""
    is_monitored = True
    state_names = {False : 'Stopped', True : 'Running'}

    @classmethod
    def from_xml(cls, node):
        """Construct the class from an xml node."""
        name = None
        locked = False
        documents = set()
        new_object = cls(None, '', '')
        for child in node.childNodes:
            if child.tagName == 'name':
                new_object.name = child.firstChild.nodeValue
            elif child.tagName == 'path':
                new_object.path = child.firstChild.nodeValue
            elif child.tagName == 'state':
                new_object.state = child.firstChild.nodeValue
            elif child.tagName == 'locked':
                locked = child.firstChild.nodeValue
            elif child.tagName == 'documents':
                for document_node in child.childNodes:
                    documents.add(Document.from_xml(document_node, new_object))

        if not new_object.name:
            new_object.name = os.path.basename(path).title()

        if locked:
            new_object.locked = True
        if documents:
            new_object.documents = documents

        return new_object

    def __init__(self, name, path, state):
        """Inititialise."""
        self.name = name
        self.path = path
        self.documents = set()
        self.state = state
        self.locked = False
        self.popup_menu = gtk.Menu()
        
        item = gtk.MenuItem('Always Ignore')
        item.connect("activate", self.save_current_state_cb)
        self.popup_menu.append(item)
        item.show()

        item = gtk.MenuItem('Ignore for this Session')
        item.connect("activate", self.save_current_state_cb)
        self.popup_menu.append(item)
        item.show()

        item = gtk.SeparatorMenuItem()
        self.popup_menu.append(item)
        item.show()

        item = gtk.MenuItem('Save Current State')
        item.connect("activate", self.save_current_state_cb)
        self.popup_menu.append(item)
        item.show()

    def __str__(self):
        "Returns a string representation of the object."""
        result = 'Application: %s\n' % (self.name)
        result += '       Path: %s\n' % (self.path)
        result += '      State: %s\n' % (self.state)
        if self.documents:
            result += '      Documents: %s' % ('\n             '.join([file.name for file in self.documents]))
        return result

    def get_documents(self):
        """Returns the list of documents associated with this application."""
        return [item for item in self.documents]

    children = property(get_documents)

    def to_xml(self, document):
        """Return an xml node representing this reference."""
        node = document.createElement('application')

        name_node = document.createElement('name')
        name_node.appendChild(document.createTextNode(str(self.name)))
        node.appendChild(name_node)

        path_node = document.createElement('path')
        path_node.appendChild(document.createTextNode(str(self.path)))
        node.appendChild(path_node)

        state_node = document.createElement('state')
        state_node.appendChild(document.createTextNode(str(self.state)))
        node.appendChild(state_node)

        if self.locked:
            locked_node = document.createElement('locked')
            locked_node.appendChild(document.createTextNode(str(self.locked)))
            node.appendChild(locked_node)

        if self.documents:
            documents_node = document.createElement('documents')
            for item in self.documents:
                documents_node.appendChild(item.to_xml(document))
            node.appendChild(documents_node)

        return node

    def restore(self):
        """Restore the application to the last recorded state."""
        if self.state == 'Running':
            find_application_item(self.path).open()
        for doc in self.documents:
            doc.restore()

    def remove(self, item):
        """Removes the given item from the list of documents associated with this application."""
        result = False
        if item in self.documents:
            self.documents.remove(item)
            result = True
        return result

    def save_current_state_cb(self, item):
        """Respond to 'Save Current State' request."""
        self.use_current_state()

    def use_current_state(self):
        """Sets the recorded state for this application to the current state."""
        app = find_application_item(self.path)
        self.state = app.state
        if hasattr(app, 'get_open_documents'):
            for item in app.get_open_documents():
                self.documents.add(Document(item.path, item, item.name))

    def is_active(self):
        """Returns a flag indicating the current state of the application."""
        app = find_application(self.path)
        if app and app.is_running():
            result = True
        else:
            result = False
        return result

    def open(self):
        """Opens the application."""
        app = find_application_item(self.path)
        app.open()

class Reference(object):
    """Manage a URL that points to a reference relevant to this session."""
    state = ''
    locked = False
    is_monitored = True
    state_names = {False : '', True : ''}
    root_pattern = re.compile("(^www\.)|\.(com|org|net|edu|mil)(\..+)?")

    @classmethod
    def from_xml(cls, node):
        """Construct the class from an xml node."""
        title = ''
        last_visit = None
        locked = False
        for child in node.childNodes:
            if child.tagName == 'url':
                url = str(child.firstChild.nodeValue)
            elif child.tagName == 'title':
                title = str(child.firstChild.nodeValue)
            elif child.tagName == 'last_visit':
                last_visit = child.firstChild.nodeValue
                last_visit = datetime.datetime.strptime(last_visit, '%c')
            elif child.tagName == 'locked':
                locked = str(child.firstChild.nodeValue)

        new_object = cls(url, title, last_visit)
        if locked:
            new_object.locked = locked

        return new_object

    def __init__(self, url, title='', last_visit=None):
        """Inititialise."""
        self.url = url
        self.title = title
        self.last_visit = last_visit
        self.locked = False
        self.children = set()

    def get_path(self):
        """Returns the URL as the path of this item."""
        return self.url

    path = property(get_path)

    @property
    def root(self):
        """Return the root elements urls."""
        url = urlparse.urlsplit(self.url)

        return '%s://%s' % (url.scheme, url.netloc)

    @property
    def index(self):
        """Return the string to index urls."""
        url = urlparse.urlsplit(self.url)
        index = Reference.root_pattern.sub('', url.netloc)
        if not index:
            index = url.scheme
        return index

    def __str__(self):
        """Returns a string representation of the link."""
        result = '      Link: %s\n' % (self.title)
        result += '       url: %s\n' % (self.url)
        result += 'Last Visit: %s\n' % (self.last_visit.strftime('%s') if self.last_visit else 'Unknown')
        return result

    def to_xml(self, document):
        """Return an xml node representing this reference."""
        node = document.createElement('reference')

        url_node = document.createElement('url')
        url_node.appendChild(document.createTextNode(str(self.url)))
        node.appendChild(url_node)

        if self.title:
            title_node = document.createElement('title')
            title_node.appendChild(document.createTextNode(str(self.title)))
            node.appendChild(title_node)

        if self.last_visit:
            last_visit_node = document.createElement('last_visited')
            last_visit_node.appendChild(document.createTextNode(self.last_visit.strftime('%c')))
            node.appendChild(last_visit)

        return node

    def get_name(self):
        """Return the name of this reference."""
        if self.title:
            name = self.title
        else:
            name = os.path.basename(self.url)
        name = name.replace('%20', ' ')
        return name

    name = property(get_name)

    def restore(self):
        """Dummy function for session item interface."""
        pass

    def is_active(self):
        """Returns false so that references are only saved if explicitly locked."""
        return False

    def open(self):
        """Display the url using existing window if possible."""
        show_reference(self.path)

class Session(object):
    """Management of session information."""
    __initialised = False

    @classmethod
    def from_xml(cls, node):
        """Load session data from an XML node."""
        name = None
        state_items = set()
        references = []
        for child_node in node.childNodes:
            if child_node.tagName == 'name':
                name = child_node.firstChild.nodeValue
            elif child_node.tagName == 'application':
                state_items.add(Application.from_xml(child_node))
            elif child_node.tagName == 'reference':
                references.append(Reference.from_xml(child_node))

        if name or len(state_items) > 0 or len(references):
            new_session = cls(name)
            new_session.stored_state = state_items
            for reference in references:
                new_session.add_reference(reference, new_session.stored_state)
        else:
            new_session = None
        return new_session

    def __init__(self, name=None):
        """Initialise the session to an inactive state."""
        # If the session is not initialised the state is read to initialise the
        # state data for applications running at startup
        if not Session.__initialised:
            state = get_system_state()
            [item.has_changed for item in state]
            Session.__initialised == True
        self.__is_active = False
        self.name = name
        self.applications_node = None
        self.links_node = None
        self.references = {}
        self.stored_state = set()
        self.initial_state = get_system_state()
        self.update_hooks = set()


    def to_xml(self, document):
        """Generate XML string representing the content of the snapshot."""
        if self.__is_active:
            self.update()

        node = None
        if self.stored_state:
            node = document.createElement('session')
            for item in self.stored_state:
                node.appendChild(item.to_xml(document))
        return node
       
    def to_string(self):
        """Return a string representation of the session state."""
        result = '%s' % (self.name)
        result += '\n'.join([item.to_string() for item in self.stored_state])
        return result
 
    def start(self):
        """Starts the session, recording the current system state."""
        if not self.__is_active:
            self.initial_state = get_system_state()
            self.start_time = datetime.datetime.now()
            start_system_monitor()

        self.__is_active = True

    def update(self):
        """Updates the session's state based on the current system state."""
        changed = False
        if self.__is_active:
            current_state = get_system_state()
            
            new_items = []
            for item in current_state:
                item.update()
                if item.has_changed:
                    new_items.append(item)

            state_item_from_path = dict([(item.path, item) for item
                                         in self.stored_state
                                         if item.is_monitored])

            for system_item in new_items:
                changed = True
                if system_item.path in state_item_from_path:
                    state_item = state_item_from_path[system_item.path]
                    state_item.state = system_item.state

                    doc_from_path = [doc.path for doc in state_item.children]

                    # Add new documents to the recorded state
                    for doc in system_item.children:
                        if doc.path not in doc_from_path:
                            document = Document(doc.path,
                                                state_item,
                                                doc.name,
                                                doc.state)
                            state_item.documents.add(document)

                    open_docs = [doc.path for doc in system_item.children]

                    # Update the state of known documents
                    for doc in state_item.children:
                        doc.state = doc.state_names[doc.path in open_docs]                     
                else:
                    if isinstance(system_item, NT_Application):
                        state_item = Application(system_item.name,
                                                 system_item.path,
                                                 system_item.state)
                        if len(system_item.children) > 0:
                            documents = set()
                            for child in system_item.children:
                                path = child.path
                                if hasattr(child, 'title'):
                                    name = child.title
                                else:
                                    name = os.path.splitext(os.path.basename(path))[0]
                                state = child.state
                                documents.add(Document(path, state_item, name, state))
                            state_item.documents = documents
                        self.stored_state.add(state_item)
                    else: # instance must be visited url:
                        new_reference = Reference(system_item.url,
                                                  system_item.title)
                        self.add_reference(new_reference, self.stored_state)
                        
        if changed and self.update_hooks:
            for hook in self.update_hooks:
                hook(self)

    def add_reference(self, reference, state):
        """Adds an indexed reference to this session."""
        state.add(reference)
        if reference.index not in self.references:
            root = Reference(reference.root, reference.index)
            self.references[reference.index] = root
        else:
            root = self.references[reference.index]

        if reference is not root:
            root.children.add(reference)

    def activate_item(self, item, parent):
        """Opens / runs the given item."""
        item.open()

    def toggle_record_cb(self, button):
        """Toggle the state of session recording."""
        if self.__is_active:
            button.set_label(gtk.STOCK_MEDIA_RECORD)
            button.set_tooltip_text('Start monitoring of session information.')
        else:
            button.set_label(gtk.STOCK_MEDIA_PAUSE)
            button.set_tooltip_text('Pause monitoring of session information.')

        
        self.__is_active = not self.__is_active

    def revert_cb(self, button):
        """Handle a click of the revert button."""
        self.restore_state()

    def restore_state(self):
        """Restore the system to the state when session stopped."""
        if self.stored_state:
            for item in self.stored_state:
                item.restore()

    def get_view(self):
        """Return list of items associated with this session."""
        items = []

        state = self.stored_state        

        applications = []
        for item in state:
            if isinstance(item, Application):
                applications.append(item)

        if applications:
            if self.applications_node:
                self.applications_node.update(applications)
            else:
                self.applications_node = List_Node('Applications', applications)
            items.append(self.applications_node)

        if self.references:
            if self.links_node:
                self.links_node.update(self.references.values())
            else:
                self.links_node = List_Node('References', self.references.values())
            items.append(self.links_node)
        return items

    children = property(get_view)

    def button_cb(self, widget, event):
        """If right button display context menu."""
        if event.type == gtk.gdk.BUTTON_RELEASE and event.button == 3:
            list_element = widget.get_path_at_pos(int(event.x), int(event.y))
            path = list_element[0] if list_element else None
            if path:
                model = widget.get_model()
                iter = model.get_iter(path)
                item = model.get_value(iter, 0)
                if hasattr(item, 'popup_menu'):
                    item.popup_menu.popup(None, None, None, 1,
                                          gtk.get_current_event_time())

    def popup_menu_cb(self, widget):
        """Display the popup menu for the state item under the mouse."""
        pass

    def get_state_data(self, item, attribute, parent):
        """Returns a string representing the requested attribute of the referenced state item."""
        attr_map = {'Name' : item.name,
                    'Stored State' : self.get_stored_state(item, parent),
                    'Locked' : str(item.locked),
                    'Current State' : self.get_current_state(item, parent)}
        return attr_map[attribute]

    def get_stored_state(self, item, parent):
        """Return the stored state of the given item."""
        state = 'N/A'
        if item in self.stored_state:
            state = item.state
        elif parent in self.stored_state:
            state = item.state
        else:
            paths = dict([(i.path, i) for i in self.stored_state])
            if item.path in paths:
                state = paths[item.path].state
            elif parent and parent.path in paths:
                for child in paths[parent.path].children:
                    if child.path == item.path:
                        state = child.state
        if isinstance(item, List_Node):
            state = ''
        return state

    def get_current_state(self, item, parent):
        """Return the current state for the given item."""
        return item.state_names[item.is_active()]

    def remove_item(self, item, parent):
        """Remove the item from the appropriate list."""
        removed = False
        if parent and hasattr(parent, 'remove'):
            removed = parent.remove(item)
        else:
            removed = True
            if item in self.stored_state:
                self.stored_state.remove(item)
            elif hasattr(item, 'children'):
                for child in item.children:
                    if child in self.stored_state:
                        self.stored_state.remove(child)

            if hasattr(item, 'index'):
                if self.references[item.index] is item:
                    del self.references[item.index]
                else:
                    self.references[item.index].children.remove(item)

        return removed

    def get_state(self):
        """Return the state, updating if necessary."""
        if self.__is_active:
            self.update()
        return self.state

    def get_persistent(self):
        """Get the nodes representing the persistent state of this session."""
        node = Node('session')
        node.children.extend([app.get_persistent() for app in self.applications])
        for link in self.links:
            link_node = Node('link')
            link_node.children.append(str(link))
            node.children.append(link_node)
        return node       

    def stop(self):
        """Stop the session."""
        if self.__is_active:
            self.update()
            self.update_hooks = set()
            self.__is_active = False
            stop_system_monitor()
